﻿using System;
using System.Text;

namespace Dpy.Math.Algebra
{
    public class Matrix
    {
        #region Fields

        private readonly double[,] matrix;
        private bool hasEvenSwaps;

        #endregion


        #region Properties

        public double this[int row, int column]
        {
            get
            {
                return this.matrix[row, column];
            }
            set
            {
                this.matrix[row, column] = value;
            }
        }

        public int Rows
        {
            get
            {
                return this.matrix.GetLength(0);
            }
        }

        public int Columns
        {
            get
            {
                return this.matrix.GetLength(1);
            }
        }

        public double Determinant
        {
            get
            {
                if (this.Rows != this.Columns)
                {
                    throw new ArgumentException("Matrix must be quadratic to calculate its Determinant!");
                }

                Matrix clonedMatrix = this.Clone();
                this.hasEvenSwaps = true;

                clonedMatrix.SwapRowsExecuted += AttacherToSwapsCounter;
                clonedMatrix.Triangulate();
                clonedMatrix.SwapRowsExecuted -= AttacherToSwapsCounter;

                double product = this.hasEvenSwaps ? 1 : -1;
                for (int i = 0; i < this.Rows; i++)
                {
                    product *= clonedMatrix[i, i];
                }

                return product;
            }
        }

        public int Rank
        {
            get
            {
                int rank = System.Math.Min(this.Rows, this.Columns);

                Matrix cloned = this.Clone();
                cloned.TrianglulateRowsBelowMainDiagonal();
                cloned.TriangulateColumnsAboveMainDiagonal();

                for (int i = rank - 1; i >= 0; i--)
                {
                    if (cloned[i,i].IsZero())
                    {
                        rank--;
                    }
                }

                return rank;
            }
        }

        public Matrix Transponse
        {
            get
            {
                double[,] matrix = new double[this.Columns, this.Rows];

                for (int i = 0; i < this.Rows; i++)
                {
                    for (int j = 0; j < this.Columns; j++)
                    {
                        matrix[j, i] = this.matrix[i, j];
                    }
                }

                return new Matrix(matrix);
            }
        }

        public Matrix Inverse
        {
            get
            {
                if (this.Rows != this.Columns)
                {
                    throw new ArgumentException("You may calculate inverse of a quadratic matrix only!");
                }

                bool hasInverse = true;
                Matrix inverse = Matrix.Identity(this.Rows);
                Matrix cloned = this.Clone();
                inverse.AttachToAnotherMatrixEvents(cloned);

                cloned.TrianglulateRowsBelowMainDiagonal();
                for (int i = 0; i < Rows; i++)
                {
                    if (cloned[i, i].IsZero())
                    {
                        hasInverse = false;
                        break;
                    }

                    cloned.MultiplyRowByNumber(i, 1 / cloned[i, i]);

                    for (int j = i - 1; j >= 0; j--)
                    {
                        cloned.AddRowToAnotherRow(i, j, -cloned[j, i]);
                    }
                }

                inverse.DetachFromAnotherMatrixEvents(cloned);

                return hasInverse ? inverse : null;
            }
        }

        #endregion


        #region Constructors

        public Matrix(double[,] matrix)
        {
            if (matrix == null || matrix.Length <= 0)
            {
                throw new ArgumentException("Matrix must have at least 1 dimension.");
            }

            this.matrix = new double[matrix.GetLength(0), matrix.GetLength(1)];

            for (int i = 0; i < this.Rows; i++)
            {
                for (int j = 0; j < this.Columns; j++)
                {
                    this.matrix[i, j] = matrix[i, j];
                }
            }
        }

        public Matrix(int rows, int columns)
            : this(new double[rows, columns])
        {
        }

        #endregion


        #region Methods

        public Matrix Clone()
        {
            return new Matrix(this.matrix);
        }

        public static bool AreEssentiallyEqual(Matrix a, Matrix b, double epsilon = Utility.Epsilon)
        {
            if (a.Rows != b.Rows || a.Columns != b.Columns)
            {
                throw new ArgumentException("Matrixes must be of same size to compare them!");
            }

            for (int i = 0; i < a.Rows; i++)
            {
                for (int j = 0; j < a.Columns; j++)
                {
                    if (!a[i, j].EssentiallyEquals(b[i, j], epsilon))
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        public static Matrix Identity(int dimension)
        {
            Matrix identity = Matrix.Zero(dimension, dimension);

            for (int i = 0; i < dimension; i++)
            {
                identity[i, i] = 1;
            }

            return identity;
        }

        public static Matrix Zero(int rows, int columns)
        {
            return new Matrix(rows, rows);
        }

        public bool IsZero(double epsilon = Utility.Epsilon)
        {
            return Matrix.AreEssentiallyEqual(this, Matrix.Zero(this.Rows, this.Columns), epsilon);
        }

        public bool IsZeroRow(int rowIndex, double epsilon = Utility.Epsilon)
        {
            for (int j = 0; j < this.Columns; j++)
            {
                if (!this[rowIndex, j].IsZero())
                {
                    return false;
                }
            }

            return true;
        }

        public bool IsZeroColumn(int columnIndex, double epsilon = Utility.Epsilon)
        {
            for (int i = 0; i < this.Rows; i++)
            {
                if (!this[i, columnIndex].IsZero())
                {
                    return false;
                }
            }

            return true;
        }

        public bool IsIdentity(double epsilon = Utility.Epsilon)
        {
            if (this.Rows != this.Columns)
            {
                throw new ArgumentException("Matrix has to be quadratic to compare it with Identity matrix!");
            }

            return Matrix.AreEssentiallyEqual(this, Matrix.Identity(this.Rows), epsilon);
        }

        public void SwapRows(int firstIndex, int secondIndex)
        {
            double swap;

            for (int j = 0; j < this.Columns; j++)
            {
                swap = this[firstIndex, j];
                this[firstIndex, j] = this[secondIndex, j];
                this[secondIndex, j] = swap;
            }

            if (this.SwapRowsExecuted != null)
            {
                OnSwapRowsExecuted(new FunctionEventArgs(new object[] { firstIndex, secondIndex }));
            }
        }

        public void SwapColumns(int firstIndex, int secondIndex)
        {
            double swap;

            for (int i = 0; i < this.Rows; i++)
            {
                swap = this[i, firstIndex];
                this[i, firstIndex] = this[i, secondIndex];
                this[i, secondIndex] = swap;
            }

            if (this.SwapColumnsExecuted != null)
            {
                OnSwapColumnsExecuted(new FunctionEventArgs(new object[] { firstIndex, secondIndex }));
            }
        }

        public void AddRowToAnotherRow(int firstRow, int secondRow, double coeficient = 1)
        {
            for (int j = 0; j < this.Columns; j++)
            {
                this[secondRow, j] += (coeficient * this[firstRow, j]);
            }

            if (this.AddRowToAnotherRowExecuted != null)
            {
                OnAddRowToAnotherRowExecuted(new FunctionEventArgs(new object[] { firstRow, secondRow, coeficient }));
            }
        }

        public void AddColumnToAnotherColumn(int firstColumn, int secondColumn, double coeficient = 1)
        {
            for (int i = 0; i < this.Rows; i++)
            {
                this[i, secondColumn] += (coeficient * this[i, firstColumn]);
            }

            if (this.AddColumnToAnotherColumnExecuted != null)
            {
                OnAddColumnToAnotherColumnExecuted(new FunctionEventArgs(new object[] { firstColumn, secondColumn, coeficient }));
            }
        }

        public void MultiplyRowByNumber(int rowIndex, double number)
        {
            for (int j = 0; j < this.Columns; j++)
            {
                this[rowIndex, j] *= number;
            }

            if (this.MultiplyRowByNumberExecuted != null)
            {
                OnMultiplyRowByNumberExecuted(new FunctionEventArgs(new object[] { rowIndex, number }));
            }
        }

        public void MultiplyColumnByNumber(int columnIndex, double number)
        {
            for (int i = 0; i < this.Rows; i++)
            {
                this[i, columnIndex] *= number;
            }

            if (this.MultiplyColumnByNumberExecuted != null)
            {
                OnMultiplyColumnByNumberExecuted(new FunctionEventArgs(new object[] { columnIndex, number }));
            }
        }

        public Vector RowVector(int rowIndex)
        {
            Vector result = new Vector(this.Columns);
            for (int j = 0; j < this.Columns; j++)
            {
                result[j] = this[rowIndex, j];
            }

            return result;
        }

        public Vector ColumnVector(int columnIndex)
        {
            Vector result = new Vector(this.Rows);
            for (int i = 0; i < this.Rows; i++)
            {
                result[i] = this[i, columnIndex];
            }

            return result;
        }

        public void Triangulate()
        {
            this.TrianglulateRowsBelowMainDiagonal();
        }

        public void TrianglulateRowsBelowMainDiagonal()
        {
            int minDimension = System.Math.Min(this.Rows, this.Columns);

            for (int column = 0; column < minDimension; column++)
            {
                int nonZeroRow = FirstNonZeroElementUnderMainDiagonal(column);

                if (nonZeroRow > column)
                {
                    this.SwapRows(nonZeroRow, column);
                }
                else if (nonZeroRow < column)
                {
                    continue;
                }

                for (int row = column + 1; row < this.Rows; row++)
                {
                    this.AddRowToAnotherRow(column, row, -this[row, column] / this[column, column]);
                }
            }
        }

        public void TriangulateColumnsAboveMainDiagonal()
        {
            int minDimension = System.Math.Min(this.Rows, this.Columns);

            for (int row = 0; row < minDimension; row++)
            {
                int nonZeroColumn = FirstNonZeroElementRightFromMainDiagonal(row);

                if (nonZeroColumn > row)
                {
                    this.SwapColumns(nonZeroColumn, row);
                }
                else if (nonZeroColumn < row)
                {
                    continue;
                }

                for (int column = row + 1; column < this.Columns; column++)
                {
                    this.AddColumnToAnotherColumn(row, column, -this[row, column] / this[row, row]);
                }
            }
        }

        public void TriangulateColumnsBelowMainDiagonal()
        {
            int minDimension = System.Math.Min(this.Rows, this.Columns);

            for (int row = 0; row < minDimension; row++)
            {
                int nonZeroColumn = FirstNonZeroElementLeftFromMainDiagonal(row);

                if (nonZeroColumn < row && nonZeroColumn >= 0)
                {
                    this.SwapColumns(nonZeroColumn, row);
                }
                else if (0 > nonZeroColumn || nonZeroColumn > row)
                {
                    continue;
                }

                for (int column = row - 1; column >= 0; column--)
                {
                    this.AddColumnToAnotherColumn(row, column, -this[row, column] / this[row, row]);
                }
            }
        }

        public void TriangulateRowsAboveMainDiagonal()
        {
            int minDimension = System.Math.Min(this.Rows, this.Columns);

            for (int column = 0; column < minDimension; column++)
            {
                int nonZeroRow = FirstNonZeroElementAboveMainDiagonal(column);

                if (nonZeroRow < column && nonZeroRow >= 0)
                {
                    this.SwapRows(nonZeroRow, column);
                }
                else if (0 > nonZeroRow || nonZeroRow > column)
                {
                    continue;
                }

                for (int row = column - 1; row >= 0; row--)
                {
                    this.AddRowToAnotherRow(column, row, -this[row, column] / this[column, column]);
                }
            }
        }

        private int FirstNonZeroElementRightFromMainDiagonal(int row)
        {
            for (int column = row; column < this.Columns; column++)
            {
                if (!this[row, column].IsZero())
                {
                    return column;
                }
            }

            return -1;
        }

        private int FirstNonZeroElementLeftFromMainDiagonal(int row)
        {
            for (int column = row; column >= 0; column--)
            {
                if (!this[row, column].IsZero())
                {
                    return column;
                }
            }

            return -1;
        }

        private int FirstNonZeroElementAboveMainDiagonal(int column)
        {
            for (int row = column; row >= 0; row--)
            {
                if (!this[row, column].IsZero())
                {
                    return row;
                }
            }

            return -1;
        }        

        private int FirstNonZeroElementUnderMainDiagonal(int column)
        {
            for (int row = column; row < this.Rows; row++)
            {
                if (!this[row, column].IsZero())
                {
                    return row;
                }
            }

            return -1;
        }

        public bool EssentiallyEquals(Matrix other)
        {
            return Matrix.AreEssentiallyEqual(this, other);
        }

        #endregion


        #region Operators

        public static Matrix operator +(Matrix a, Matrix b)
        {
            if (a.Rows != b.Rows || a.Columns != b.Columns)
            {
                throw new ArgumentException("Matrixes must have same dimensions!");
            }

            Matrix result = new Matrix(a.Rows, a.Columns);

            for (int i = 0; i < a.Rows; i++)
            {
                for (int j = 0; j < a.Columns; j++)
                {
                    result[i, j] = a[i, j] + b[i, j];
                }
            }
            return result;
        }

        public static Matrix operator -(Matrix a)
        {
            return -1 * a;
        }

        public static Matrix operator -(Matrix a, Matrix b)
        {
            if (a.Rows != b.Rows || a.Columns != b.Columns)
            {
                throw new ArgumentException("Matrixes must have same dimensions!");
            }

            return a + (-b);
        }

        public static Matrix operator *(double number, Matrix a)
        {
            Matrix result = new Matrix(a.Rows, a.Columns);

            for (int i = 0; i < a.Rows; i++)
            {
                for (int j = 0; j < a.Columns; j++)
                {
                    result[i, j] = number * a[i, j];
                }
            }

            return result;
        }

        public static Matrix operator *(Matrix a, double number)
        {
            return number * a;
        }

        public static Matrix operator *(Matrix a, Matrix b)
        {
            if (a.Columns != b.Rows)
            {
                throw new ArgumentException("First matrix columns number must be equal to second matrix rows number!");
            }

            Matrix result = new Matrix(a.Rows, b.Columns);

            for (int i = 0; i < a.Rows; i++)
            {
                for (int j = 0; j < b.Columns; j++)
                {
                    for (int k = 0; k < a.Columns; k++)
                    {
                        result[i, j] += a[i, k] * b[k, j];
                    }
                }
            }

            return result;
        }

        #endregion


        #region Overriden methods

        public override string ToString()
        {
            StringBuilder result = new StringBuilder();

            string listSeparator = Dpy.Math.Utility.CurrentCulture.TextInfo.ListSeparator;

            result.AppendFormat("(({0}", this[0, 0]);

            for (int j = 1; j < this.Columns; j++)
            {
                result.AppendFormat("{0} {1}", listSeparator, this[0, j]);
            }

            result.Append(")");

            for (int i = 1; i < this.Rows; i++)
            {
                result.AppendLine(listSeparator);

                result.AppendFormat("({0}", this[i, 0]);

                for (int j = 1; j < this.Columns; j++)
                {
                    result.AppendFormat("{0} {1}", listSeparator, this[i, j]);
                }

                result.Append(")");
            }

            result.Append(")");

            return result.ToString();
        }

        #endregion


        #region Attaching to and Detaching from events

        private void AttacherToSwapsCounter(object sender, FunctionEventArgs e)
        {
            this.hasEvenSwaps = !this.hasEvenSwaps;
        }

        internal void AttachToAnotherMatrixEvents(Matrix matrix)
        {
            matrix.SwapColumnsExecuted += AttacherToSwapColumns;
            matrix.SwapRowsExecuted += AttacherToSwapRows;
            matrix.MultiplyRowByNumberExecuted += AttacherToMultiplyRowByNumber;
            matrix.MultiplyColumnByNumberExecuted += AttacherToMultiplyColumnByNumber;
            matrix.AddColumnToAnotherColumnExecuted += AttacherToAddColumnToAnotherColumn;
            matrix.AddRowToAnotherRowExecuted += AttacherToAddRowToAnotherRow;
        }

        internal void DetachFromAnotherMatrixEvents(Matrix matrix)
        {
            matrix.SwapColumnsExecuted -= AttacherToSwapColumns;
            matrix.SwapRowsExecuted -= AttacherToSwapRows;
            matrix.MultiplyRowByNumberExecuted -= AttacherToMultiplyRowByNumber;
            matrix.MultiplyColumnByNumberExecuted -= AttacherToMultiplyColumnByNumber;
            matrix.AddColumnToAnotherColumnExecuted -= AttacherToAddColumnToAnotherColumn;
            matrix.AddRowToAnotherRowExecuted -= AttacherToAddRowToAnotherRow;
        }

        private void AttacherToAddRowToAnotherRow(object sender, FunctionEventArgs e)
        {
            this.AddRowToAnotherRow((int)e.FunctionArguments[0], (int)e.FunctionArguments[1], (double)e.FunctionArguments[2]);
        }

        private void AttacherToAddColumnToAnotherColumn(object sender, FunctionEventArgs e)
        {
            this.AddColumnToAnotherColumn((int)e.FunctionArguments[0], (int)e.FunctionArguments[1], (double)e.FunctionArguments[2]);
        }

        private void AttacherToSwapColumns(object s, FunctionEventArgs e)
        {
            this.SwapColumns((int)e.FunctionArguments[0], (int)e.FunctionArguments[1]);
        }

        private void AttacherToMultiplyRowByNumber(object s, FunctionEventArgs e)
        {
            this.MultiplyRowByNumber((int)e.FunctionArguments[0], (double)e.FunctionArguments[1]);
        }

        private void AttacherToMultiplyColumnByNumber(object s, FunctionEventArgs e)
        {
            this.MultiplyColumnByNumber((int)e.FunctionArguments[0], (double)e.FunctionArguments[1]);
        }

        private void AttacherToSwapRows(object s, FunctionEventArgs e)
        {
            this.SwapRows((int)e.FunctionArguments[0], (int)e.FunctionArguments[1]);
        }

        #endregion


        #region Events

        public event FunctionExecutedEventHandler MultiplyRowByNumberExecuted;

        protected virtual void OnMultiplyRowByNumberExecuted(FunctionEventArgs functionArguments)
        {
            if (MultiplyRowByNumberExecuted != null)
            {
                MultiplyRowByNumberExecuted(this, functionArguments);
            }
        }


        public event FunctionExecutedEventHandler MultiplyColumnByNumberExecuted;

        protected virtual void OnMultiplyColumnByNumberExecuted(FunctionEventArgs functionArguments)
        {
            if (MultiplyColumnByNumberExecuted != null)
            {
                MultiplyColumnByNumberExecuted(this, functionArguments);
            }
        }


        public event FunctionExecutedEventHandler AddRowToAnotherRowExecuted;

        protected virtual void OnAddRowToAnotherRowExecuted(FunctionEventArgs functionArguments)
        {
            if (AddRowToAnotherRowExecuted != null)
            {
                AddRowToAnotherRowExecuted(this, functionArguments);
            }
        }


        public event FunctionExecutedEventHandler AddColumnToAnotherColumnExecuted;

        protected virtual void OnAddColumnToAnotherColumnExecuted(FunctionEventArgs functionArguments)
        {
            if (AddColumnToAnotherColumnExecuted != null)
            {
                AddColumnToAnotherColumnExecuted(this, functionArguments);
            }
        }


        public event FunctionExecutedEventHandler SwapRowsExecuted;

        protected virtual void OnSwapRowsExecuted(FunctionEventArgs functionArguments)
        {
            if (SwapRowsExecuted != null)
            {
                SwapRowsExecuted(this, functionArguments);
            }
        }


        public event FunctionExecutedEventHandler SwapColumnsExecuted;

        protected virtual void OnSwapColumnsExecuted(FunctionEventArgs functionArguments)
        {
            if (SwapColumnsExecuted != null)
            {
                SwapColumnsExecuted(this, functionArguments);
            }
        }

        #endregion

    }
}
